package com.sky.service.impl;

import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.sky.constant.MessageConstant;
import com.sky.constant.PasswordConstant;
import com.sky.constant.StatusConstant;
import com.sky.context.BaseContext;
import com.sky.dto.EmployeeDTO;
import com.sky.dto.EmployeeLoginDTO;
import com.sky.dto.EmployeePageQueryDTO;
import com.sky.entity.Employee;
import com.sky.exception.AccountLockedException;
import com.sky.exception.AccountNotFoundException;
import com.sky.exception.PasswordErrorException;
import com.sky.mapper.EmployeeMapper;
import com.sky.result.PageResult;
import com.sky.service.EmployeeService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;

import java.time.LocalDateTime;
import java.util.List;

//Service是业务层
@Service
public class EmployeeServiceImpl implements EmployeeService {

    @Autowired
    private EmployeeMapper employeeMapper;

    /**
     * 员工登录
     *
     * @param employeeLoginDTO
     * @return
     */
    public Employee login(EmployeeLoginDTO employeeLoginDTO) {
        String username = employeeLoginDTO.getUsername();
        String password = employeeLoginDTO.getPassword();

        //1、根据用户名查询数据库中的数据
        Employee employee = employeeMapper.getByUsername(username);
        //这里就将前端获取到的用户名"admin"作为参数传入到getByUsername,然后在传递给sql语句中的参数,就可以实现查询

        //2、处理各种异常情况（用户名不存在、密码不对、账号被锁定）
        if (employee == null) {
            //账号不存在
            throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND);
            //这个异常,放到了sky-common的目录,对应的exception文件夹中
        }

        //密码比对
        //对前端传过来的明文密码进行md5加密处理
            //我回顾java基础笔记_yue,对应的mysql章节的时候,看到了有md5加密的函数,原来就是md5,没想到
            //当然mysql的md5加密只相当于一个函数,直接作用在sql语句上的,和这个不同,但本质都是md5用来加密的呀
         password = DigestUtils.md5DigestAsHex(password.getBytes());
        //DigestUtils这是spring提供的一个工具类,专门用来MD5密码加密
        //password.getBytes() 表示将密码password转换成一个数组,然后再赋值给他自己;
            //注意:前面的password就没必要再用String password来声明变量了(.var默认会给你声明好),会报错
        if (!password.equals(employee.getPassword())) {
            //前面这个password表示前端传过来的密码,然后经过md5加密处理了;
                //而后面的employee.getPassword()是获取到数据库中存储的密码,然后二者进行比对
            //密码错误
            throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
            //这里只要抛出异常,,就会被GlobalExceptionHandler这个全局异常处理器捕获到,
            //这个异常,也是放到了sky-common的目录,对应的exception文件夹中
            //后面传入的就是对应的异常常量信息,MessageConstant.PASSWORD_ERROR
                //对应的是sky-common对应的constant目录中的MessageConstant常量类,里面定义了很多信息
        }

        if (employee.getStatus() == StatusConstant.DISABLE) {
            //账号被锁定
            //上面常量定义StatusConstant,默认为1是账号启用,如果是0那就是账号被禁用
            throw new AccountLockedException(MessageConstant.ACCOUNT_LOCKED);
            //这个异常,也是放到了sky-common的目录,对应的exception文件夹中
        }

        //3、返回实体对象
            //将上面getByUsername方法通过sql语句查询到的结果,进行返回
            //谁调用,就返回给谁;在Controller层employeeService.login调用的当前的这个方法,就返回给他
        return employee;
    }

    /**
     * 实现类里实现:新增员工
     * @param employeeDTO
     */
    @Override
    public void save(EmployeeDTO employeeDTO) {
        //打印出当前线程的id
        System.out.println("当前线程的id:" + Thread.currentThread().getId());
        //Thread.currentThread().getId()通过这个Thread的方法就能获取到当前线程的id

        //老师说用DTO是为了方便我们封装前端提交过来的数据,但是传递给我们的持久层,mapper,建议我们转换成对应的实体类
        //先new个employee实体,用来转换
        Employee employee = new Employee();

        //对employee中的属性值进行设置;而具体的属性值信息在前端传过来的employeeDTO就有
        //employee.setName(employeeDTO.getName());
        //上面这样一个个属性设置太繁琐了,因为这个实体类中的属性和DTO中的很多都是重复的,所以用对象的属性拷贝

        // 对象属性拷贝
        //BeanUtils是spring提供的一个工具类
        BeanUtils.copyProperties(employeeDTO,employee);
        //上面从employeeDTO中的属性拷贝到employee这个实体类中(能用属性拷贝,是因为有相同属性名)
        //剩下DTO相比于实体类缺少的属性,就得自己手动设置

        //设置账号的状态,默认是正常的状态,1表示正常,0表示锁定
        employee.setStatus(StatusConstant.ENABLE);
        //这里参数不直接写1,是为了避免硬编码,所以这里用的是StatusConstant常量类,ENABLE就是对应数值1,表示正常的状态

        //设置密码,默认密码123456
        employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));
        //DigestUtils.md5DigestAsHex前面用过,就是直接把明文密码进行md5加密; 需要注意,后面要getBytes()变成byte数组
        //补充:MD5加密,处理的是二进制的数据,所以才要把上面的字符串转换成字节数组(byte[]);
            // getBytes() 是字符串类的一个方法，它用于将字符串转换成字节数组
        //这个默认密码,也不要写死,所以用的常量类 PasswordConstant.DEFAULT_PASSWORD

        //设置当前记录的创建时间和修改时间
            //下面4个公共属性的赋值,是因为在自定义的AutoFillAspect切面类中,把这4个公共属性,单独拿出来,通过反射来赋值的,
                // 所以下面这里属性的赋值,就注释掉了
        //employee.setCreateTime(LocalDateTime.now());
            //LocalDateTime.now()就是系统的当前时间
        //employee.setUpdateTime(LocalDateTime.now());

        //设置当前记录的创建人id和修改人id
        //employee.setCreateUser(BaseContext.getCurrentId());
        //employee.setUpdateUser(BaseContext.getCurrentId());
            //需要注意,上面这个id是怎么动态获取的------
                // 通过ThreadLocal提供的方法,在令牌拦截器JwtTokenAdminInterceptor类中,通过jwt令牌解析到的id,
                // 存放到对应线程的存储空间,为啥要这样操作;因为前端每次的请求,都是一个独立的线程,那存储空间也是独立的
                //为啥用BaseContext.getCurrentId()来实现,用的是BaseContext类而不是直接用ThreadLocal的方法调取;
                // 因为在BaseContext类中定义了ThreadLocal相关的方法,逻辑更清晰;这个类在sky-common对应的context目录下


        //调用mapper对数据库操作
        employeeMapper.insert(employee);
    }

    /**
     * 分页查询--在当前实现类实现service接口中这个方法的定义
     * @param employeePageQueryDTO
     */
    @Override
    public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
        //select * from employee limit 0,10   表示从第0+1开始取记录,取10行数据---就分为1页,进行分页
        //补充插件pagehelper(在父模块pom.xml配置文件中已经引入了)---作用: 简化上面limit后面的代码,就是分页的计算问题
            //正常情况下,后面可能要考虑去计算具体的页数,,怎么分第几页?
                //要考虑每页显示记录数*(第几页-1),每页显示记录数;
                // 即第2页就是10*(2-1),10 即10,10;从第11条数据开始就是第2页
        //这个limit就是服务于分页查询的,看Java笔记_yue,P1045分页查询;  就能清楚的知道0,10的意义是start,rows

        //开始分页查询
            //这里能直接使用PageHelper,是因为在父模块pom.xml配置文件中进行管理;而在对应的子模块pom.xml中都引入了
        PageHelper.startPage(employeePageQueryDTO.getPage(),employeePageQueryDTO.getPageSize());
        //表示开始分页----;
        // 传入对应的页码:employeePageQueryDTO.getPage();
        // 传入每页显示的记录数: employeePageQueryDTO.getPageSize()
        //上面这两个参数在用接口文档测试的时候,比如请求页码为1,记录数为5,那就表示查询页码为1,当前页有5条数据的请求

        //mapper中实现对应的sql语句查询
        Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);
        //需要注意,因为上面用了PageHelper插件,所以前面这里的返回结果+泛型必须是Page<实体类>  要遵守人家的规则
        //疑问:  你看上面这行代码,并没有把第151行PageHelper相关参数传入到当前代码行,那它是怎么自动分页的呢
            //老师带着追进去源码,151行底层,也是用到了ThreadLocal,前面讲到过,
                // 会将对应的limit后面的两个参数存储到独立的存储空间;然后执行pageQuery的sql语句进行查询的时候,
                //就会动态的将limit的两个参数(比如上面的0,10)从独立的线程空间拿出来,放到sql语句中,就实现了分页查询

        //因为最终我们要返回的是PageResult对象,而上面用的是PageHelper插件,返回的是Page,
            // 所以要想办法转换成PageResult对象;
            // 打开PageResult类,有total(总记录数),records(当前页数据集合)两个属性,
            // 所以当前如果能用page获取到这两个属性,就能封装成PageResult对象
        long total = page.getTotal();   //这个就是获取到的总的记录数
        List<Employee> records = page.getResult();   //这个就是获取到的当前页数据集合
        //下面把得到的这两个属性,封装成PageResult对象;  下面老师直接放到return 里面去了,单独拿出来封装也可以
        return new PageResult(total,records);
        //你看封装一个对象,直接new一个对象,然后传入对应的属性就可以(相当于调用的是有参的构造器);
        // 自己怎么想不到,真好,真开心
        
        //我丢,,这里的分页查询,为啥没有使用属性拷贝的功能;我知道了,这里没让封装VO类,并且,VO也没有继承Employee类
            //所以不用属性拷贝
    }

    /**
     * 启用/禁用员工账号
     * @param status
     * @param id
     */
    @Override
    public void startOrStop(Integer status, Long id) {
        //本质上是要修改数据库中员工对应的字段状态--(那这里请求方式还用post,不懂不懂)
        //update employee set status = ? where id = ?
        //老师说为了通用性,设计成动态的参数,,即多个字段都可以修改,根据你传进的参数修改,就比较通用

       //动态参数,那下面就不能直接传指定写死的参数了,所以老师说这里要传实体类,要创建实体类
        //因为在实体类中添加了构建器,@Builder,所以不用像传统的那样new对象,然后在set传入参数加入到new的实例中去

        //传统的第一种写法:
        //Employee employee = new Employee();
        //employee.setStatus(status);
        //employee.setId(id);

        //直接用@Builder的第二种方式
            //前面用过builder(在EmployeeController这个类中"员工登录模块"对应的EmployeeLoginVO设置的属性)
        Employee employee = Employee.builder()       //这里直接获取到builder构建器对象,下面就可以直接设置属性
                .status(status)                     //直接传入了对应参数
                .id(id)
                .build();

        employeeMapper.update(employee);
    }

    /**
     * 根据id来查询员工信息:用于对话框的数据回显
     * @param id
     * @return
     */
    @Override
    public Employee getById(Long id) {
        Employee employee = employeeMapper.getById(id);
        //对密码进行处理,就是说加密过的密码也不想让你普通用户看
        employee.setPassword("****");
        //这里相当于把密码进行了处理.设置成了****,那你就不知道真正密码了
            //不过肯定不会影响到day01讲解的员工登陆的加密过后的密码
        return employee;
    }

    /**
     * 编辑员工信息
     * @param employeeDTO
     */
    @Override
    public void update(EmployeeDTO employeeDTO) {
        //在前面"启用/禁用员工账号"的时候,就实现了一个动态的update语句(所有字段都有),
            // 所以这里可以直接调用这个方法来进行修改
        //但是我们在EmployeeMapper.xml中定义的接收参数是Employee对象,所以这里要先进行一下转换--通过对象属性拷贝
        Employee employee = new Employee();
        //使用对象属性拷贝
        BeanUtils.copyProperties(employeeDTO,employee);
        //剩下把employeeDTO中没有的updateTime,updateUser等等进行添加
            //下面两个属性,在自定义的AutoFillAspect切面类中,公共变量进行了统一赋值(通过反射),所以下面代码就注释掉了
        //employee.setUpdateTime(LocalDateTime.now());
            //LocalDateTime.now()表示当前系统时间
        //employee.setUpdateUser(BaseContext.getCurrentId());
            //BaseContext.getCurrentId()用来获取当前当前修改人的id,前面这个工具类都讲过,这里想不起来要调用它

        employeeMapper.update(employee);
    }
}
